home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 8614 / 8614.xpi / chrome / extension.jar / content / document_manager / DocumentManager.js
Encoding:
Text File  |  2010-02-10  |  26.4 KB  |  998 lines

  1. Glydo.DocumentManager = Prototype.Class.create(Glydo.EventSource,{
  2.     initialize: function($super) {
  3.         $super();
  4.         this.browsers = new Array();
  5.         this.listeners = new Array();
  6.         this.nextBrowserId = 0;
  7.         this.nextDocumentId = 0;
  8.         // FIXME: fix this, it is not initialized correctly
  9.         this.currentBrowserEntry = null;
  10.         this.cache = new Glydo.DocumentManager.Cache();
  11.         if (Glydo.Prefs.offline_source_dir) {
  12.             this.offlineSource = new Glydo.DocumentManager.OfflineSource();
  13.         }
  14.         if (Glydo.Prefs.offline_log_dir && 
  15.                 (Glydo.Prefs.offline_log_with_offline_source || !this.offlineSource)) {
  16.             this.offlineLog = new Glydo.DocumentManager.OfflineLog();
  17.         }
  18.     },
  19.  
  20.     onMainDocDomLoad: function(browser,doc) {
  21.         if (this.containsDocument(doc)) {
  22.             return;
  23.         }
  24.         var browserEntry = this.findOrCreateBrowserEntry(browser);
  25.         var newDocumentEntry = new Glydo.DocumentManager.DocumentEntry(browserEntry,doc,this.nextDocumentId++);
  26.         browserEntry.onNewDocument(newDocumentEntry);
  27.         this.fire("onNewDocument",newDocumentEntry);
  28.         if (this.currentBrowserEntry === browserEntry) {
  29.             this.fire("onCurrentDocumentChanged",newDocumentEntry);
  30.         }
  31.         this.fireStateChange();
  32.     },
  33.  
  34.     onTabOpen: function(browser) {
  35.     },
  36.     
  37.     onTabClose: function(browser) {
  38.         var found = this.findBrowserIndex(browser);
  39.         if (found !== null) {
  40.             this.browsers.splice(found,1);
  41.         }
  42.     },
  43.     
  44.     onTabSelect: function(browser) {
  45.         this.currentBrowserEntry = this.findOrCreateBrowserEntry(browser);
  46.         this.fire("onCurrentDocumentChanged",this.currentBrowserEntry.currentDocumentEntry);
  47.         this.fire("onCurrentProcessedDocumentChanged",this.currentBrowserEntry.currentProcessedDocumentEntry);
  48.     },
  49.     
  50.     containsDocument: function(doc) {
  51.         for (var i = 0; i < this.browsers.length; ++i) {
  52.             if (this.browsers[i].containsDocument(doc)) {
  53.                 return true;
  54.             }
  55.         }
  56.         return false;
  57.     },
  58.     
  59.     findBrowserIndex: function(browser) {
  60.         for (var i = 0; i < this.browsers.length; ++i) {
  61.             if (this.browsers[i].browser === browser) {
  62.                 return i;
  63.             }
  64.         }
  65.         return null;
  66.     },
  67.     
  68.     findBrowserEntry: function(browser) {
  69.         var found = this.findBrowserIndex(browser);
  70.         if (found === null) {
  71.             return null;
  72.         }
  73.         return this.browsers[found];
  74.     },
  75.         
  76.     findOrCreateBrowserEntry: function(browser) {
  77.         for (var i = 0; i < this.browsers.length; ++i) {
  78.             if (this.browsers[i].browser === browser) {
  79.                 return this.browsers[i];
  80.             }
  81.         }
  82.         var browserEntry = new Glydo.DocumentManager.BrowserEntry(this,browser,this.nextBrowserId++);
  83.         this.browsers.push(browserEntry);
  84.         return browserEntry;
  85.     },
  86.     
  87.     getCurrentDocument: function() {
  88.         if (this.currentBrowserEntry) {
  89.             if (this.currentBrowserEntry.currentDocumentEntry) {
  90.                 return this.currentBrowserEntry.currentDocumentEntry;
  91.             }
  92.         }
  93.         return null;
  94.     },
  95.     
  96.     getCurrentProcessedDocument: function() {
  97.         if (this.currentBrowserEntry) {
  98.             if (this.currentBrowserEntry.currentProcessedDocumentEntry) {
  99.                 return this.currentBrowserEntry.currentProcessedDocumentEntry;
  100.             }
  101.         }
  102.         return null;
  103.     },
  104.     
  105.     isCurrentDocument: function(documentEntry) {
  106.         return this.getCurrentDocument() === documentEntry;
  107.     },
  108.     
  109.     isCurrentBrowser: function(browserEntry) {
  110.         return this.currentBrowserEntry === browserEntry;
  111.     },
  112.     
  113.     isCurrentProcessedDocument: function(documentEntry) {
  114.         return this.getCurrentProcessedDocument() === documentEntry;
  115.     },
  116.     
  117.     fireStateChange: function(browser) {
  118.         this.fire('onDocumentManagerStateChange');
  119.     },
  120.     
  121.     containsValidDocuments: function() {
  122.         var nBrowsers = this.browsers.length;
  123.         for (var i = 0; i < nBrowsers; ++i) {
  124.             if (this.browsers[i].currentProcessedDocumentEntry) {
  125.                 return true;
  126.             }
  127.         }
  128.         return false;
  129.     },
  130.     
  131.     containsUnrenderedValidDocuments: function() {
  132.         var nBrowsers = this.browsers.length;
  133.         for (var i = 0; i < nBrowsers; ++i) {
  134.             if (this.browsers[i].currentProcessedDocumentEntry && !d.rendered) {
  135.                 return true;
  136.             }
  137.         }
  138.         return false;
  139.     },
  140.     
  141.     isProcessing: function() {
  142.         if (!this.currentBrowserEntry) {
  143.             return false;
  144.         } 
  145.         if (!this.currentBrowserEntry.currentDocumentEntry) {
  146.             return false;
  147.         } 
  148.         return !this.currentBrowserEntry.currentDocumentEntry.isDone();
  149.     },
  150.     
  151.     isCurrentDocumentValid: function() {
  152.         if (!this.currentBrowserEntry) {
  153.             return false;
  154.         } 
  155.         if (!this.currentBrowserEntry.currentDocumentEntry) {
  156.             return false;
  157.         } 
  158.         return this.currentBrowserEntry.currentDocumentEntry.isDone();
  159.     },
  160.     
  161.     isTrackableURL: function(documentURI)
  162.     {
  163.         return Glydo.DocumentManager.TRACKABLE_URL_PATTERN.test(documentURI);
  164.     }
  165.     
  166. });
  167.  
  168. /////////////////////////////////////////////////////////////////////////
  169. // BrowserEntry class contains the state of all documents
  170. // in the history of browser. Documents are added as the browser
  171. // traverses documents, and removed after both the following take place:
  172. // 1. The document was unloaded
  173. // 2. The document is old enough to discard
  174. /////////////////////////////////////////////////////////////////////////
  175.  
  176. Glydo.DocumentManager.BrowserEntry = Prototype.Class.create({
  177.     initialize: function(manager,browser,id) {
  178.         this.manager = manager;
  179.         this.browser = browser;
  180.         this.id = id;
  181.     //    this.oldDocs = new Array();
  182.         this.currentDocumentEntry = null;
  183.         this.currentProcessedDocumentEntry = null;
  184.     },
  185.  
  186.     getCache: function() {
  187.         return this.manager ? this.manager.cache : null;
  188.     },
  189.  
  190.     getOfflineSource: function() {
  191.         return this.manager ? this.manager.offlineSource : null;
  192.     },
  193.     
  194.     getOfflineLog: function() {
  195.         return this.manager ? this.manager.offlineLog : null;
  196.     },
  197.     
  198.     containsDocument: function(doc) {
  199.         if (this.currentDocumentEntry === doc) {
  200.             return true;
  201.         }
  202.         //    for (var i = 0; i < this.oldDocs.length; ++i) {
  203.         //        if (this.oldDocs[i].document === doc) {
  204.         //            return true;
  205.         //        }
  206.         //    }
  207.         return false;
  208.     },
  209.     
  210.     onNewDocument: function(docEntry) {
  211.         
  212.         if (this.currentDocumentEntry) {
  213.             this.currentDocumentEntry.cancelProcessing();
  214.         }
  215.         this.currentDocumentEntry = docEntry;
  216.         this.currentDocumentEntry.startProcessing();
  217.     },
  218.     
  219.     isTrackableURL: function(documentURI) {
  220.         return this.manager.isTrackableURL(documentURI);
  221.     },
  222.  
  223.     onDoneProcessing: function(docEntry) {
  224.         docEntry.logRequest();
  225.         if (!docEntry.cancelled) {
  226.             if (docEntry === this.currentDocumentEntry) {
  227.                 
  228.                 if (docEntry.hasRecommendations()) {
  229.                     
  230.                 } else if (docEntry.state == 'SUCCEEDED') {
  231.                     
  232.                 } else {
  233.                     
  234.                 }
  235.                 this.currentProcessedDocumentEntry = docEntry;
  236.                 if (this.manager.isCurrentBrowser(this)) {
  237.                     this.manager.fire("onCurrentProcessedDocumentChanged",docEntry);
  238.                 }
  239.             }
  240.         }
  241.         this.manager.fireStateChange();
  242.     },
  243.     
  244. });
  245.  
  246. /////////////////////////////////////////////////////////////////////////
  247. //DocumentEntry class contains the state of a specific document
  248. //loaded in a browser.
  249. /////////////////////////////////////////////////////////////////////////
  250.  
  251. Glydo.DocumentManager.DocumentEntry = Prototype.Class.create({
  252.     initialize: function(manager,doc,id) {
  253.         this.manager = manager;
  254.         this.document = doc;
  255.         this.id = id;
  256.         this.recommendations = null;
  257.         this._hasRecommendations = false;
  258.         this.state = "CREATED";
  259.         this.cancelled = false;
  260.         this.rendered = false;
  261.         this.requestTime = null;
  262.         this.responseTime = null;
  263.         this.resultSource = null;
  264.         this.fromCache = false;
  265.         this.extractionTask = null;
  266.         this.viewSpecificData = {};
  267.     },
  268.  
  269.     getViewSpecificData: function(key) {
  270.         return this.viewSpecificData[key];
  271.     },
  272.     
  273.     setViewSpecificData: function(key,value) {
  274.         this.viewSpecificData[key] = value;
  275.     },
  276.     
  277.     getCache: function() {
  278.         return this.manager ? this.manager.getCache() : null;
  279.     },
  280.  
  281.     getOfflineSource: function() {
  282.         return this.manager ? this.manager.getOfflineSource() : null;
  283.     },
  284.     
  285.     getOfflineLog: function() {
  286.         return this.manager ? this.manager.getOfflineLog() : null;
  287.     },
  288.     
  289.     cancelProcessing: function() {
  290.         if (!this.isDone()) {
  291.             this.cancelled = true;
  292.             if (this.extractionTask) {
  293.                 this.extractionTask.cancel();
  294.             }
  295.         }
  296.     },
  297.     
  298.     startProcessing: function() {
  299.     
  300.         this.startProcessingTime = new Date();
  301.         
  302.         if (!this.document.documentURI) {
  303.             this.state = "SUCCEEDED";
  304.             this.manager.onDoneProcessing(this);
  305.             return;
  306.         }
  307.  
  308.         
  309.         
  310.         // Check for an appropriate result in the cache
  311.         var cached = this.getCache().lookup(this.document.documentURI);
  312.         if (cached) {
  313.             
  314.             this.loadFromCachedResult(cached);
  315.             return;
  316.         }
  317.  
  318.         // Check for an offline source
  319.         if (this.getOfflineSource()) {
  320.             var offline = this.getOfflineSource().lookup(this.document.documentURI);
  321.             if (offline) {
  322.                 
  323.                 this.loadFromOfflineResult(offline);
  324.                 return;
  325.             }
  326.             
  327.         }
  328.         
  329.         if (!this.manager.isTrackableURL(this.document.documentURI)) {
  330.             this.state = "SUCCEEDED";
  331.             this.manager.onDoneProcessing(this);
  332.             return;
  333.         }
  334.         
  335.         if (Glydo.Utils.isPrivateBrowsingEnabled()) {
  336.             this.state = "SUCCEEDED";
  337.             this.manager.onDoneProcessing(this);
  338.             return;
  339.         }
  340.         
  341.         this.resultSource = "server";
  342.         this.fromCache = false;
  343.         
  344.         this.contextItems = [];
  345.         this.contextExtractionTask = Glydo.CONTEXT_EXTRACTOR.extract(this.document,{
  346.             onContextItem: Prototype.F.bind(this.onContextItemExtracted,this),
  347.             onCompleted: Prototype.F.bind(this.onContextExtractionDone,this),
  348.             onFailure: Prototype.F.bind(this.onFailure,this),
  349.         });
  350.     },
  351.         
  352.     onFailure: function(reason) {
  353.         this.state = "FAILED";
  354.         this.error = reason;
  355.         this.manager.onDoneProcessing(this);
  356.     },
  357.     
  358.     onAjaxFailure: function(response) {
  359.         var reason = response ? response.responseText : "Communication problem";
  360.         if (!reason) {
  361.             reason = "Communication problem";
  362.         }
  363.         this.onFailure(reason);
  364.     },
  365.     
  366.     onContextItemExtracted: function(contextItem) {
  367.         if (typeof(contextItem) == "object" && contextItem != null) {
  368.             // Generate a unique id for the context item and store it
  369.             var id = Glydo.Utils.uuid1();
  370.             this.contextItems[id] = contextItem;
  371.         }
  372.     },
  373.     
  374.     encodeContextInfo: function() {
  375.         var items = [];
  376.         items.__itemName = "item";
  377.         for (var id in this.contextItems) {
  378.             var c = this.contextItems[id];
  379.             c["@id"] = id;
  380.             items.push(c);
  381.         }
  382.         if (items.length == 0) {
  383.             return null;
  384.         }
  385.         items = ({
  386.             "context": {
  387.                 "page": {
  388.                     "title":this.document.title 
  389.                 },
  390.                 "items": items
  391.             }
  392.         });
  393.         var ctxDoc = document.implementation.createDocument("","",null);
  394.         Glydo.Utils.toXml(ctxDoc,items);
  395.         var serializer = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"].createInstance(Components.interfaces.nsIDOMSerializer);
  396.         return serializer.serializeToString(ctxDoc);
  397.     },
  398.     
  399.     onContextExtractionDone: function(contextInfo) {
  400.         // Check for cancellation
  401.         if (this.cancelled) {
  402.             this.state = "FAILED";
  403.             this.error = "Cancelled";
  404.             this.manager.onDoneProcessing();
  405.             return;
  406.         }
  407.         var encodedContextInfo = this.encodeContextInfo();
  408.         if (encodedContextInfo == null) {
  409.             this.onFailure("No context information");
  410.             return;
  411.         }
  412.         // Create request parameters
  413.         var params = new Object();
  414.         params.clientId = Glydo.CLIENT_INFO.clientId;
  415.         params.action = "get";
  416.         params.format = "json";
  417.         params.url    = this.document.documentURI;
  418.         params.rawContextInfo = encodedContextInfo;
  419.         //
  420.         var e = Glydo.Prefs.engines;
  421.         if (e.length > 0) {
  422.             params.engines = e.join(",");
  423.         }
  424.         var p = Glydo.Prefs.publishers;
  425.         if (p.length > 0) {
  426.             params.publishers = p.join(",");
  427.         }
  428.         params.appId = Glydo.CLIENT_INFO.appId;
  429.         if (Glydo.Prefs.server_trace) {
  430.             params.useTrace = 1;
  431.         }
  432.     
  433.         
  434.         // Create a new AJAX request for the new location
  435.         this.ajax_request = new Prototype.Ajax.Request(
  436.                 Glydo.Prefs.server_url, {
  437.                 method: 'post',
  438.                 parameters: params,
  439.                 onSuccess: Prototype.F.bind(this.onGetRecommendationsSuccess,this),
  440.                 onFailure: Prototype.F.bind(this.onAjaxFailure,this)
  441.             });
  442.         this.requestTime = new Date();
  443.         
  444.         this.state = "GETTING_RECOMMENDATIONS";
  445.     },
  446.     
  447.     onGetRecommendationsSuccess: function(response) {
  448.         if (!response || (response.status == 0)) {
  449.             this.onAjaxFailure(response);
  450.             return;
  451.         }
  452.         this.responseTime = new Date();
  453.         if (this.cancelled) {
  454.             this.state = "FAILED";
  455.             this.error = "Cancelled";
  456.             this.manager.onDoneProcessing(this);
  457.             return;
  458.         }
  459.         
  460.         //
  461.         this.recommendations = response.responseJSON;
  462.         this.state = "SUCCEEDED";
  463.         if (this.getOfflineLog()) {
  464.             this.getOfflineLog().log(this);
  465.         }
  466.         var valid = this.processRecommendationsPreCacheSave();
  467.         if (valid) {
  468.             if (!this.fromCache) {
  469.                 this.getCache().save(this);
  470.             }
  471.             this.processRecommendationsPostCacheSave();
  472.         }
  473.         this.manager.onDoneProcessing(this);
  474.     },
  475.     
  476.     processRecommendationsPreCacheSave: function() {
  477.         if (!this.recommendations) {
  478.             return false;
  479.         }
  480.         
  481.         if (this.recommendations.documentAnalysis) {
  482.             var contextAnnotations = this.recommendations.documentAnalysis.contextAnnotations;
  483.             if (contextAnnotations && Prototype.O.isArray(contextAnnotations)) {
  484.                 var nca = contextAnnotations.length;
  485.                 for (var ica = 0; ica < nca; ++ica) {
  486.                     var ca = contextAnnotations[ica];
  487.                     var id = ca["id"];
  488.                     
  489.                     this.contextItems[id].annotation = ca;
  490.                 }
  491.             }
  492.         }
  493.         
  494.         var recSet = this.recommendations.recommendationsSet;
  495.         if (recSet === undefined) {
  496.             
  497.             return false;
  498.         }
  499.         
  500.         var recSetId = recSet.id;
  501.         if (recSetId === undefined) {
  502.             
  503.             return false;
  504.         }
  505.         
  506.         var recs = recSet.recommendations;
  507.         if (recs == undefined) {
  508.             
  509.             return false;
  510.         }
  511.     
  512.         var nrecs = Math.min(recs.length,Glydo.Prefs.max_recs_from_server_to_process);
  513.  
  514.         recs.splice(nrecs);
  515.         
  516.         for (var i = 0; i < nrecs; ++i) {
  517.             var rec = recs[i];
  518.             rec.recSetId = recSetId;
  519.             rec.globalId = rec.recSetId + "_" + rec.id;
  520.             rec.receivedTime = this.responseTime;
  521.             rec.viewSpecificInfo = {};
  522.             this.markRecReceived(rec);
  523.             this.setupImageLoaders(rec);
  524.             this._hasRecommendations = true;
  525.         }
  526.         
  527.         // This is a bit of a hack. We compute here virtual recommendations for
  528.         // "info" recs with identical terms
  529.  
  530.         // FIXME: For now we do this by title, but it's wrong. Missing
  531.         // required data from the server
  532.         var infoRecsByTerm = {};
  533.         for (var i = 0; i < nrecs; ++i) {
  534.             var rec = recs[i];
  535.         }
  536.             
  537.         return true;
  538.     },
  539.  
  540.     setupImageLoaders: function(rec) {
  541.         rec.imageLoaders = {};
  542.         this.loadRecommendationThumbnails(rec);
  543.         this.loadRecommendationFavicon(rec);
  544.     },
  545.  
  546.     loadRecommendationImageInternal: function(rec,key,thumbnailGroups,targetWidth,targetHeight,options) {
  547.        if (!rec.imageLoaders[key]) {
  548.             rec.imageLoaders[key] = new Glydo.BestThumbnailLoader(
  549.                     targetWidth, targetHeight,
  550.                     thumbnailGroups, options);
  551.         }
  552.     },
  553.  
  554.     loadRecommendationThumbnails: function(rec) {
  555.         var thumbnailGroups;
  556. //        thumbnailGroups = [rec.thumbnails].concat([[{
  557. //            width: 80,
  558. //            height: 60,
  559. //            lazy: true,
  560. //            url: Glydo.gApp.generateThumbnailServiceURL(rec)
  561. //        }]]);
  562.         thumbnailGroups = [rec.thumbnails];
  563.         if (rec.contentType == "video") {
  564.             this.loadRecommendationImageInternal(rec,"thumbnail-or-capture:120x90",
  565.                     thumbnailGroups,120,90); 
  566.         } else if (rec.contentType == "info") {
  567.             this.loadRecommendationImageInternal(rec,"thumbnail-or-capture:60x60",
  568.                     thumbnailGroups,60,60);
  569.         } else if (rec.contentType == "twitter") {
  570.             if (rec.thumbnails && rec.thumbnails.length > 0) {
  571.                 this.loadRecommendationImageInternal(rec,"thumbnail:48x48",
  572.                         [rec.thumbnails],48,48); 
  573.             }
  574.         } else {
  575.             this.loadRecommendationImageInternal(rec,"thumbnail-or-capture:80x60",
  576.                     thumbnailGroups,80,60);
  577.         }
  578.         // Load origin thumbnails as well
  579.         if (rec.origin) {
  580.             this.loadRecommendationImageInternal(rec,"origin:thumbnail:90x40",[rec.origin.thumbnails],90,40,{
  581.                 validation: Glydo.DocumentManager.VALIDATE_ORIGIN_IMAGE_FUNC
  582.             });
  583.         }
  584.     },
  585.  
  586.     loadRecommendationFavicon: function(rec) {
  587.         var imageKey = "favicon:16x16"; 
  588.         var thumbnailGroups = Glydo.Utils.getFaviconURLs(rec.url,rec.website).map(
  589.             function(url) {
  590.                 return ([{
  591.                     width: 16,
  592.                     height: 16,
  593.                     url: url
  594.                 }]);
  595.             });
  596.          
  597.         this.loadRecommendationImageInternal(rec,imageKey,thumbnailGroups,16,16); 
  598.     },
  599.  
  600.     processRecommendationsPostCacheSave: function() {
  601.         var recSet = this.recommendations.recommendationsSet;
  602.         var recs = recSet.recommendations;
  603.         
  604.         var nrecs = recs.length;
  605.     
  606.         for (var i = 0; i < nrecs; ++i) {
  607.             var rec = recs[i];
  608.             rec.documentEntry = this;
  609.             if (rec.contextItemId) {
  610.                 rec.contextItem = this.contextItems[rec.contextItemId];
  611.             }
  612.         }
  613.     },
  614.  
  615.     loadFromOfflineResult: function(result) {
  616.         this.resultSource = "offline";
  617.         this.onGetRecommendationsSuccess(result);
  618.     },
  619.     
  620.     loadFromCachedResult: function(result) {
  621.         for (var property in result) {
  622.             this[property] = result[property];
  623.         }
  624.         if (!this.resultSource) {
  625.             this.resultSource = "cache";
  626.         }
  627.         this.fromCache = true;
  628.         this.processRecommendationsPostCacheSave();
  629.         this.state = "SUCCEEDED";
  630.         this.manager.onDoneProcessing(this);
  631.     },
  632.     
  633.     selectRecommendations: function(filter) {
  634.         if (!this.recommendations  || !this.recommendations.recommendationsSet || !this.recommendations.recommendationsSet.recommendations) {
  635.             return [];
  636.         }
  637.         if (Prototype.O.isArray(filter)) {
  638.             return filter;
  639.         }
  640.         var de = this;
  641.         var result = this.recommendations.recommendationsSet.recommendations;
  642.         if (filter.condition !== undefined) {
  643.             // First, filter the recommendations
  644.             var filterFunc;
  645.             if (Prototype.O.isFunction(filter.condition)) {
  646.                 filterFunc = filter.condition;
  647.             } else {
  648.                 filterFunc = function(rec) {
  649.                     for (key in filter.condition) {
  650.                         var cond = filter.condition[key];
  651.                         if (Prototype.O.isFunction(cond)) {
  652.                             return cond.call(de,rec[key])
  653.                         } else {
  654.                             if (!Prototype.O.isArray(cond)) {
  655.                                 cond = [cond];
  656.                             }
  657.                             if (cond.indexOf(rec[key]) === -1) {
  658.                                 return false;
  659.                             }
  660.                         }
  661.                     }
  662.                     return true;
  663.                 };
  664.             }
  665.             result = result.filter(filterFunc,this);
  666.         }
  667.         // Next, sort them
  668.         if (filter.orderBy !== undefined) {
  669.             if (Prototype.O.isFunction(filter.orderBy)) {
  670.                 result = Prototype.A.sortBy(result,filter.orderBy);
  671.             } else {
  672.                 var orderBy = Prototype.O.isArray(filter.orderBy) ? filter.orderBy : [filter.orderBy];
  673.                 result.sort(function(rec1,rec2) {
  674.                     var comp = 0;
  675.                     orderBy.every(function(key) {
  676.                         var desc = Prototype.S.startsWith(key,"-");
  677.                         if (desc || Prototype.S.startsWith(key,"+")) {
  678.                             key = key.substring(1);
  679.                         }
  680.                         var v1 = rec1[key];
  681.                         var v2 = rec2[key];
  682.                         if (v1 < v2) {
  683.                             comp = desc ? 1 : -1;
  684.                             return false;
  685.                         } else if (v1 > v2) {
  686.                             comp = desc ? -1 : 1;
  687.                             return false;
  688.                         }
  689.                         return true;
  690.                     });
  691.                     return comp;
  692.                 });
  693.             }
  694.         }
  695.  
  696.         // Finally, truncate the result if necessary
  697.         if (filter.limit !== undefined) {
  698.             result = result.slice(0,filter.limit);
  699.         }
  700.         return result;
  701.     },
  702.  
  703.     isDone: function() {
  704.         return this.isSucceeded() || this.isFailed();
  705.     },
  706.     
  707.     isProcessed: function() {
  708.         return this.isDone() && !this.cancelled;
  709.     },
  710.     
  711.     isSucceeded: function() {
  712.         return this.state == 'SUCCEEDED';
  713.     },
  714.     
  715.     isFailed: function() {
  716.         return this.state == 'FAILED';
  717.     },
  718.     
  719.     hasRecommendations: function() {
  720.         return this.isDone() && this._hasRecommendations;
  721.     },
  722.     
  723.     markRendered: function() {
  724.         this.rendered = true;
  725.         this.manager.manager.fireStateChange();
  726.     },
  727.     
  728.     getRecSetId: function() {
  729.         if (this.recommendations && 
  730.                 this.recommendations.recommendationsSet &&
  731.                 this.recommendations.recommendationsSet.id) {
  732.             return this.recommendations.recommendationsSet.id;
  733.         }
  734.         return null;
  735.     },
  736.     
  737.     logRequest: function() {
  738.         if (!this.requestTime || (this.resultSource !== "server")) {
  739.             return;
  740.         }
  741.     
  742.         var recSetId = this.getRecSetId();
  743.         
  744.         
  745.         if (!this.fromCache) {
  746.             Glydo.LOGGING_DB.logRecsRequest(
  747.                     this.requestTime,
  748.                     this.responseTime,
  749.                     recSetId,
  750.                     this.cancelled);
  751.         } else {
  752.             // Log the cached rec request hit
  753.             Glydo.LOGGING_DB.logCachedRecsReqHit(
  754.                     this.startProcessingTime,
  755.                     recSetId);
  756.         }
  757.         
  758.     },
  759.     
  760.     markRecReceived: function(rec) {
  761.         if (!this.requestTime || this.resultSource !== "server") {
  762.             return;
  763.         }
  764.         
  765.         Glydo.LOGGING_DB.markAckedRecAsReceived(rec.url,this.responseTime);
  766.     },
  767.     
  768. });
  769.  
  770. /////////////////////////////////////////////////////////////////////////
  771. //Cache class 
  772. /////////////////////////////////////////////////////////////////////////
  773. Glydo.DocumentManager.Cache = Prototype.Class.create({
  774.     initialize: function() {
  775.         this.cacheEntryListHead = new Glydo.DocumentManager.CacheEntry();
  776.         this.cacheLookupTable = {};
  777.         this.size = 0;
  778.     },
  779.  
  780.     lookup: function(url) {
  781.         
  782.         var cacheEntry = this.cacheLookupTable[url];
  783.         if (!cacheEntry) {
  784.             
  785.             return null;
  786.         }
  787.         var now = Date.now();
  788.         if (now - cacheEntry.createdAt > Glydo.Prefs.cache_max_age_millis) {
  789.             
  790.             this.removeEntry(cacheEntry);
  791.             return null;
  792.         }
  793.         
  794.         this.markAsJustUsed(cacheEntry);
  795.         return cacheEntry.result;
  796.     },
  797.  
  798.     // save should only be called when we have a really new result
  799.     // to cache. It should not be called for old cache results.
  800.     save: function(documentEntry) {
  801.         var url = documentEntry.document.documentURI;
  802.         // Remove any old cache entry if it exists
  803.         // We do this so that a caller may overwrite the cache entry
  804.         // with new data
  805.         var oldCacheEntry = this.cacheLookupTable[url]; 
  806.         if (oldCacheEntry) {
  807.             this.removeEntry(oldCacheEntry);
  808.         }
  809.         
  810.         var m = Math.max(Glydo.Prefs.cache_max_entries,0);
  811.         while (this.size > m) {
  812.             
  813.             this.removeLeastRecentlyUsedEntry();
  814.         }
  815.         if (this.size == m && m > 0) {
  816.             
  817.             this.removeLeastRecentlyUsedEntry();
  818.         }
  819.         if (this.size < m) {
  820.             var cacheEntry = new Glydo.DocumentManager.CacheEntry(url,documentEntry);
  821.             this.addEntryAsJustUsed(cacheEntry);
  822.         }
  823.     },
  824.     
  825.     addEntryAsJustUsed: function(cacheEntry) {
  826.         this.cacheEntryListHead.insertAsNext(cacheEntry);
  827.         cacheEntry.markAsJustUsed();
  828.         this.cacheLookupTable[cacheEntry.url] = cacheEntry;
  829.         this.size++;
  830.     },
  831.     
  832.     removeEntry: function(cacheEntry) {
  833.         cacheEntry.remove();
  834.         delete this.cacheLookupTable[cacheEntry.url];
  835.         this.size--;
  836.     },
  837.  
  838.     removeLeastRecentlyUsedEntry: function() {
  839.         if (this.cacheEntryListHead.prev !== this.cacheEntryListHead) {
  840.             this.removeEntry(this.cacheEntryListHead.prev);
  841.         }
  842.     },
  843.  
  844.     markAsJustUsed: function(cacheEntry) {
  845.         this.removeEntry(cacheEntry);
  846.         this.addEntryAsJustUsed(cacheEntry);
  847.     },
  848.     
  849. });
  850.  
  851. /////////////////////////////////////////////////////////////////////////
  852. //CacheEntry class
  853. /////////////////////////////////////////////////////////////////////////
  854. Glydo.DocumentManager.CacheEntry = Prototype.Class.create({
  855.     initialize: function(url,result) {
  856.         // Create the entry as a single node in a list
  857.         this.next = this;
  858.         this.prev = this;
  859.         if (result) {
  860.             this.url = url;
  861.             this.createdAt = Date.now();
  862.             this.result = {};
  863.             this.CACHABLE_PROPERTIES.forEach(function(property) {
  864.                 this.result[property] = result[property];
  865.             },this);
  866.             this.markAsJustUsed();
  867.         }
  868.     },
  869.     
  870.     markAsJustUsed: function() {
  871.         this.lastUsedAt = Date.now();
  872.     },
  873.  
  874.     remove: function() {
  875.         var prev = this.prev;
  876.         var next = this.next;
  877.         prev.next = next;
  878.         next.prev = prev;
  879.     },
  880.     
  881.     insertAsNext: function(entry) {
  882.         var next = this.next;
  883.         this.next = entry;
  884.         next.prev = entry;
  885.         entry.next = next;
  886.         entry.prev = this;
  887.     },
  888.     
  889.     CACHABLE_PROPERTIES: ["recommendations","resultSource","_hasRecommendations","requestTime","responseTime","contextItems"]
  890.     
  891. });
  892.  
  893. /////////////////////////////////////////////////////////////////////////
  894. //OfflineSource class represents an offline recommendations source
  895. /////////////////////////////////////////////////////////////////////////
  896. Glydo.DocumentManager.OfflineSource = Prototype.Class.create({
  897.     initialize: function() {
  898.         this.responses = {};
  899.         var sourceDir = Glydo.DirIO.get("ProfD");
  900.         sourceDir.append("glydo");
  901.         sourceDir.append("offline_sources");
  902.         sourceDir.append(Glydo.Prefs.offline_source_dir);
  903.         
  904.         if (!sourceDir.exists()) {
  905.             
  906.             return;
  907.         }
  908.         var entries = sourceDir.directoryEntries;
  909.         while (entries.hasMoreElements()) {
  910.             var entry = entries.getNext();
  911.             entry = entry.QueryInterface(Components.interfaces.nsIFile);
  912.             if (entry.isFile()) {
  913.                 
  914.                 var entryStr = Glydo.FileIO.read(entry,"UTF-8");
  915.                 var entryObj = Prototype.S.decodeJSON(entryStr);
  916.                 this.responses[entryObj.url] = entryObj.response;
  917.             }
  918.         }
  919.     },
  920.  
  921.     lookup: function(url) {
  922.         
  923.         var response = this.responses[url];
  924.         if (!response) {
  925.             return null;
  926.         }
  927.         return ({
  928.             status: 200,
  929.             responseJSON: response
  930.         });
  931.     }
  932. });
  933.  
  934. /////////////////////////////////////////////////////////////////////////
  935. //OfflineSource class represents an offline logging target
  936. // for recommendation results
  937. /////////////////////////////////////////////////////////////////////////
  938. Glydo.DocumentManager.OfflineLog = Prototype.Class.create({
  939.     initialize: function() {
  940.         var logDir = Glydo.DirIO.get("ProfD");
  941.         logDir.append("glydo");
  942.         logDir.append("offline_logs");
  943.         Glydo.DirIO.create(logDir);
  944.         logDir.append(Glydo.Prefs.offline_log_dir);
  945.         Glydo.DirIO.create(logDir);
  946.         this.logDir = logDir;
  947.         
  948.     },
  949.  
  950.     log: function(documentEntry) {
  951.         
  952.         var file = this.logDir.clone();
  953.         var host = Glydo.Utils.getURLHost(documentEntry.document.documentURI);
  954.         file.append(host + ".json");
  955.         file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
  956.         
  957.         var contentsObj = ({
  958.             url: documentEntry.document.documentURI,
  959.             response: documentEntry.recommendations
  960.         });
  961.         var contents = Prototype.O.toJSON(contentsObj);
  962.         if (Glydo.FileIO.write(file,contents,null,"UTF-8")) {
  963.             
  964.         } else {
  965.             
  966.         }
  967.     }
  968. });
  969.  
  970. // CONSTANTS
  971. Glydo.DocumentManager.TRACKABLE_URL_PATTERN = new RegExp("^http://"); 
  972. Glydo.DocumentManager.DEFAULT_RECOMMENDATION_ORDER = {
  973.         maxRecommendations: 20
  974. };
  975.  
  976. Glydo.DocumentManager.ORIGIN_DOMAINS_WITH_PROBLEMATIC_IMAGES = ({
  977.         "shopping.com": true,
  978.         "bizrate.com": true
  979. });
  980.  
  981. Glydo.DocumentManager.VALIDATE_ORIGIN_IMAGE_FUNC = function(image) {
  982.     if (!Glydo.DocumentManager.ORIGIN_DOMAINS_WITH_PROBLEMATIC_IMAGES[Glydo.Utils.getHighLevelDomainName(this.url).toLowerCase()]) {
  983.         return true;
  984.     }
  985.     if (image.naturalWidth != null &&
  986.         (image.naturalWidth < 10 ||
  987.          (this.width != null && this.width > 0 && this.width != image.naturalWidth))) {
  988.         return false;
  989.     }
  990.     if (image.naturalHeight != null &&
  991.             (image.naturalHeight < 10 ||
  992.              (this.height != null && this.height > 0 && this.height != image.naturalHeight))) {
  993.         return false;
  994.     }
  995.     return true;
  996. };
  997.  
  998.